﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Web.Routing;
using MvcSupplantCaching.Annotations;

namespace MvcSupplantCaching
{
    /// <summary>
    /// Class for managing the <see cref="OutputCache"/> to allow for easy removal of cache items.
    /// </summary>
    public class OutputCacheManager : IReadWriteOutputCacheManager
    {
        private readonly BaseOutputCacheProvider _outputCacheProvider;

        /// <summary>
        /// The <see cref="BaseOutputCacheProvider"/> used by this manager.
        /// </summary>
        public BaseOutputCacheProvider OutputCacheProvider
        {
            get { return _outputCacheProvider; }
        }

        private readonly IKeyBuilder _keyBuilder;

        /// <summary>
        /// The <see cref="IKeyBuilder"/> used for this manager.
        /// </summary>
        public IKeyBuilder KeyBuilder
        {
            get { return _keyBuilder; }
        }

        /// <summary>
        /// Default constructor. Uses <see cref="OutputCache.Instance"/> for the <seealso cref="OutputCacheProvider"/>. 
        /// Instantiates a new <see cref="KeyBuilder"/> to use for making cache keys.
        /// </summary>
        private OutputCacheManager()
            : this(OutputCache.Instance, new KeyBuilder())
        {
        }

        private static OutputCacheManager _defaultInstance = new OutputCacheManager();

        public static OutputCacheManager Instance
        {
            get { return _defaultInstance; }
        }

        /// <summary>
        /// Constructs a new output cache manager from an existing <see cref="IOutputCacheManager"/>.
        /// </summary>
        /// <param name="dependencyCache">The <see cref="IDictionary"/> to use as the dependency cache rather than what is set by the provider.</param>
        public OutputCacheManager(IDictionary dependencyCache)
        {
            _requestCache = dependencyCache;
        }

        /// <summary>
        /// Fully parameterized constructor allowing the specification of an <see cref="OutputCacheProvider"/> and <see cref="IKeyBuilder"/>.
        /// </summary>
        /// <param name="outputCacheProvider">The <see cref="OutputCacheProvider"/> that will be managed.</param>
        /// <param name="keyBuilder">The <see cref="KeyBuilder"/> to use for generating cache keys.</param>
        private OutputCacheManager(BaseOutputCacheProvider outputCacheProvider, IKeyBuilder keyBuilder)
        {
            _outputCacheProvider = outputCacheProvider;
            _keyBuilder = keyBuilder;
        }

        private static OutputCacheManager _parameterizedInstance = null;

        public static OutputCacheManager GetInstance(BaseOutputCacheProvider outputCacheProvider, IKeyBuilder keyBuilder)
        {
            return _parameterizedInstance ?? (_parameterizedInstance = new OutputCacheManager(outputCacheProvider, keyBuilder));
        }

        /// <summary>
        /// Adds an item to the managed <see cref="OutputCacheProvider"/>.
        /// </summary>
        /// <param name="key">The key of the item to be added.</param>
        /// <param name="cacheItem">The <see cref="CacheItem"/> to be added to the managed <see cref="OutputCacheProvider"/></param>
        /// <param name="utcExpiry">The absolute time at which the <paramref name="cacheItem"/> will fall out of the cache.</param>
        public void AddItem(string key, CacheItem cacheItem, DateTime utcExpiry)
        {
            _outputCacheProvider.Add(key, cacheItem, utcExpiry);
        }

        /// <summary>
        /// Retrieves a <see cref="CacheItem"/> from the managed <see cref="OutputCacheProvider"/>
        /// </summary>
        /// <param name="key">The key for the <see cref="CacheItem"/> to retrieve.</param>
        /// <returns>The <see cref="CacheItem"/> retrieved from the cache. Can be null.</returns>
        public CacheItem GetItem(string key)
        {
            return _outputCacheProvider.Get(key) as CacheItem;
        }

        /// <summary>
        /// Removes a single output cache entry for the specified controller and action.
        /// </summary>
        /// <param name="controllerName">The name of the controller that contains the action method.</param>
        /// <param name="actionName">The name of the controller action method.</param>
        public void RemoveItem([AspMvcController] string controllerName, [AspMvcAction] string actionName)
        {
            RemoveItem(controllerName, actionName, null);
        }

        /// <summary>
        /// Removes a single output cache entry for the specified controller, action and parameters.
        /// </summary>
        /// <param name="controllerName">The name of the controller that contains the action method.</param>
        /// <param name="actionName">The name of the controller action method.</param>
        /// <param name="routeValues">An object that contains the parameters for a route.</param>
        public void RemoveItem([AspMvcController] string controllerName, [AspMvcAction] string actionName, object routeValues)
        {
            RemoveItem(controllerName, actionName, new RouteValueDictionary(routeValues));
        }

        /// <summary>
        /// Removes a single output cache entry for the specified controller, action and parameters.
        /// </summary>
        /// <param name="controllerName">The name of the controller that contains the action method.</param>
        /// <param name="actionName">The name of the controller action method.</param>
        /// <param name="routeValues">A dictionary that contains the parameters for a route.</param>
        public void RemoveItem([AspMvcController] string controllerName, [AspMvcAction] string actionName, RouteValueDictionary routeValues)
        {
            var key = _keyBuilder.BuildKey(controllerName, actionName, routeValues);

            _outputCacheProvider.Remove(key);
        }

        /// <summary>
        /// Removes all output cache entries.
        /// </summary>
        public void RemoveItems()
        {
            RemoveItems(null, null, null);
        }

        /// <summary>
        /// Removes all output cache entries for the specified controller.
        /// </summary>
        /// <param name="controllerName">The name of the controller.</param>
        public void RemoveItems([AspMvcController] string controllerName)
        {
            RemoveItems(controllerName, null, null);
        }

        /// <summary>
        /// Removes all output cache entries for the specified controller and action.
        /// </summary>
        /// <param name="controllerName">The name of the controller that contains the action method.</param>
        /// <param name="actionName">The name of the controller action method.</param>
        public void RemoveItems([AspMvcController] string controllerName, [AspMvcAction] string actionName)
        {
            RemoveItems(controllerName, actionName, null);
        }

        /// <summary>
        /// Removes all output cache entries for the specified controller, action and parameters.
        /// </summary>
        /// <param name="controllerName">The name of the controller that contains the action method.</param>
        /// <param name="actionName">The name of the controller action method.</param>
        /// <param name="routeValues">An object that contains the parameters for a route.</param>
        public void RemoveItems([AspMvcController] string controllerName, [AspMvcAction] string actionName, object routeValues)
        {
            RemoveItems(controllerName, actionName, new RouteValueDictionary(routeValues));
        }

        /// <summary>
        /// Removes all output cache entries for the specified controller, action and parameters.
        /// </summary>
        /// <param name="controllerName">The name of the controller that contains the action method.</param>
        /// <param name="actionName">The name of the controller action method.</param>
        /// <param name="routeValues">A dictionary that contains the parameters for a route.</param>
        public void RemoveItems([AspMvcController] string controllerName, [AspMvcAction] string actionName, RouteValueDictionary routeValues)
        {
            var enumerableCache = _outputCacheProvider as IEnumerable<KeyValuePair<string, object>>;

            if (enumerableCache == null)
            {
                throw new NotSupportedException("Ensure that your custom OutputCacheProvider implements IEnumerable<KeyValuePair<string, object>>.");
            }

            var key = _keyBuilder.BuildKey(controllerName, actionName);

            if (string.IsNullOrEmpty(key))
            {
                return;
            }

            var keysToDelete = enumerableCache
                .Where(_ => !string.IsNullOrEmpty(_.Key) && _.Key.StartsWith(key))
                .Select(_ => _.Key);

            if (routeValues != null)
            {
                foreach (var routeValue in routeValues)
                {
                    var keyFrag = _keyBuilder.BuildKeyFragment(routeValue);

                    if (string.IsNullOrEmpty(keyFrag))
                    {
                        continue;
                    }

                    keysToDelete = keysToDelete.Where(_ => !string.IsNullOrEmpty(_) && _.Contains(keyFrag));
                }
            }

            foreach (var keyToDelete in keysToDelete)
            {
                _outputCacheProvider.Remove(keyToDelete);
            }
        }

        private readonly IDictionary _requestCache;
        public virtual IDictionary RequestCache { get { return _requestCache ?? _outputCacheProvider.RequestCache; } }

        /// <summary>
        /// Adds a cache dependency on an output cached page.
        /// </summary>
        /// <param name="key">The dependency key.</param>
        public virtual void AddOutputCacheDependency(string key)
        {
            if (RequestCache == null)
            {
                throw new Exception("RequestCache property on the specified provider was null.");
            }

            if (string.IsNullOrEmpty(key))
            {
                throw new ArgumentNullException("key");
            }

            // See if it exists already, if so, add this output cache key to the list.
            var dependencyItem = RequestCache[BaseOutputCacheProvider.CacheDependenciesKey] as HashSet<string> ?? new HashSet<string>();
            if (!dependencyItem.Contains(key))
            {
                dependencyItem.Add(key);
            }
            RequestCache[BaseOutputCacheProvider.CacheDependenciesKey] = dependencyItem;
        }

        /// <summary>
        /// Writes the output dependencies to the RequestCache
        /// </summary>
        public virtual void FinalizeDependencies()
        {
            // Process the output cache dependencies
            string outputCacheKey = RequestCache[OutputCache.CacheKey] as string;
            if (string.IsNullOrEmpty(outputCacheKey))
            {
                return;
            }

            HashSet<string> dependencyKeys = RequestCache[BaseOutputCacheProvider.CacheDependenciesKey] as HashSet<string>;
            if (dependencyKeys == null)
            {
                return;
            }

            // Add a dependency for the output cache key on the current url.
            DateTime expiry = DateTime.UtcNow.AddYears(1);
            string url = RequestCache[OutputCache.UrlCacheKey] as string;
            if (!string.IsNullOrEmpty(url))
            {
                dependencyKeys.Add(url);
            }

            // Add the output dependencies for the current cache key.
            foreach (string key in dependencyKeys)
            {
                // See if it exists already, if so, add this output cache key to the list.
                var dependencyItem = OutputCache.Instance.Get(key) as CacheDependencyItem ?? new CacheDependencyItem();
                dependencyItem.RelatedOutputCacheKeys = dependencyItem.RelatedOutputCacheKeys ?? new List<string>();
                if (!dependencyItem.RelatedOutputCacheKeys.Contains(outputCacheKey))
                {
                    dependencyItem.RelatedOutputCacheKeys.Add(outputCacheKey);
                }
                if (OutputCache.Instance.Get(key) == null)
                {
                    OutputCache.Instance.Add(key, dependencyItem, expiry);
                }
            }
        }

        /// <summary>
        /// Expires the output cached pages for the given url.
        /// </summary>
        /// <param name="url">The url to expire cached pages for.</param>
        public void ExpireOutputCacheForUrl(string url)
        {
            OutputCache.Instance.Remove(url);
        }
    }
}
